import {isNone} from '@/common'
import {isArr, isEmptyArr} from '@/arr'
import {isFun} from '@/fun'


/**
 * 将数组转化为树形结构数组，在转化后的树形结构的数组中，子级元素会被添加到父级元素的 children 属性中，children 属性是一个数组类型
 *
 * @template T 原数组元素的类型
 * @template R 函数返回结果中树形结构数组元素的类型
 * @param {T[]} origin 原数组
 * @param {string} primaryFieldName 数组中元素的主键字段名
 * @param {string} parentFieldName 数组中元素用于表示其父级元素的字段名
 * @param {string} childrenFieldName 父级元素存储子级元素的 children 属性的字段名，默认为 'children'
 * @param {boolean} isPath 是否需要生成从根节点到当前子级节点的路径，默认为 false，生成的路径是由当前子级节点的主键值加上父级节点的主键值
 * 组成的数组，如果当前节点为根节点，则路径为空数组
 * @param {string} pathFieldName 需要生成从根节点到当前子级节点的路径时，用于存储路径数据的字段名，默认为 'path'
 * @returns {{tree: Array<R>, map: Record<any, R>}} 树形结构数组和映射表组成的对象，其中 tree 为树形结构数组，map 为映射表，映射表的
 * key 为数组元素的主键值，映射表的 value 为数组元素
 */
export const convertArr2Tree = <T, R>(
  origin: T[],
  primaryFieldName: string,
  parentFieldName: string,
  childrenFieldName: string = 'children',
  isPath: boolean = false,
  pathFieldName: string = 'path'
): {
  tree: Array<R>
  map: Record<any, R>
} => {
  // 最终要返回的树形结构结果数组
  const result: Array<R> = []
  // 主键与最终要返回的树形结构结果数组中的元素之间的映射
  const map: Record<any, R> = {}
  /**
   * 数组元素的处理函数
   * @param item 当前遍历到的 origin 数组元素
   * @param index 当前遍历到的 origin 数组元素的索引
   */
  const itemHandler = (item: any, index: number) => {
    // 根据当前遍历的元素生成树形结构中的节点
    const node = {...item, [childrenFieldName]: []}
    // 映射表中添加当前节点
    map[node[primaryFieldName]] = node
    // 如果当前节点没有父级
    if (!node[parentFieldName]) {
      // 如果需要生成路径，则给当前节点添加路径字段，由于当前节点没有父级，所以路径为仅包含当前节点的主键值的数组
      if (isPath) node[pathFieldName] = [node[primaryFieldName]]
      // 直接添加到最终结果数组中
      result.push(node)
      // 标记当前数组元素已处理
      item.handled = true
    }
    // 如果当前遍历的元素有父级
    else {
      itemWithParentHandler(node, item, index)
    }
  }
  /**
   * 将具有父级的节点添加到父级的 children 数组中
   * @param node 当前处理中的节点
   * @param parent 当前处理中的节点的父级
   * @param item 当前遍历到的 origin 数组元素
   */
  const itemWithParentAdd2Parent = (node: any, parent: any, item: any) => {
    // 如果需要生成路径，则给当前节点添加路径字段，将父级的路径数组与当前节点的主键值拼接成新的路径数组
    if (isPath) node[pathFieldName] = [...(parent[pathFieldName]), node[primaryFieldName]]
    // 将当前节点添加到父级的 children 数组中
    parent[childrenFieldName].push(map[node[primaryFieldName]])
    // 标记当前数组元素已处理
    item.handled = true
  }
  /**
   * 具有父级的元素的处理函数
   * @param node 当前处理中的节点
   * @param item 当前遍历到的 origin 数组元素
   * @param index 当前遍历到的 origin 数组元素的索引
   */
  const itemWithParentHandler = (node: any, item: any, index: number) => {
    // 在映射表中寻找当前节点的父级
    let parent = map[node[parentFieldName]]
    // 如果在映射表中找到父级
    if (parent) itemWithParentAdd2Parent(node, parent, item)
    // 如果在映射表中没有找到父级
    else {
      // 从前向后遍历到当前数组元素的过程中没有出现当前节点的父级，说明当前节点的父级可能在当前数组元素的后面，所以从当前数组元素的后面开始，寻找
      // 当前节点的父级，提前处理当前节点的父级
      for (let i = index + 1; i < origin.length; i++) {
        const iitem = origin[i] as T & {
          handled: boolean,
          [propName: string]: any,
        }
        // 如果当前遍历的元素已处理过，则跳过
        if (iitem.handled) continue
        // 如果不为父级
        if (iitem[primaryFieldName] !== node[parentFieldName]) continue
        // 如果是父级元素，先提前对父级元素进行处理
        itemHandler(iitem, i)
        // 在映射表中找到提前处理完成的父级
        parent = map[node[parentFieldName]]
        // 将当前节点添加到父级的 children 数组中
        itemWithParentAdd2Parent(node, parent, item)
        // 找到当前节点的父级，退出当前函数的执行
        return
      }
      // 循环执行结束，还未找到当前节点的父级，则说明当前节点的父级不存在，则直接将当前节点添加到最终结果数组中
      result.push(node)
      // 标记当前数组元素已处理
      item.handled = true
    }
  }
  // 将原先的数组转化为树形结构
  for (let i = 0; i < origin.length; i++) {
    const item = origin[i] as T & {handled: boolean}
    // 如果当前数组元素已处理过，则跳过
    if (item.handled) continue
    // 处理当前数组元素
    itemHandler(item, i)
  }
  // 返回最终的树形结构结果数组
  return {
    tree: result,
    map
  }
}


/**
 * 将树形结构数组扁平化为一维数组
 *
 * @template T 树形结构数组元素的类型
 * @template R 函数返回结果扁平化处理后的一维数组元素的类型
 * @param {Array<T>} tree 树形结构数组
 * @param {string} childrenFieldName 树形结构数组中父节点中存放子节点的字段名，默认为 'children'
 * @returns {Array<R>} 扁平化后的一维数组
 */
export const flattenTree2Arr = <T, R>(
  tree: Array<T>,
  childrenFieldName: string = 'children'
): Array<R> => {
  // 最终要返回的结果数组
  const result: Array<R> = []
  // 对树形结构数组进行扁平化的处理函数
  const flatten = (nodes: Array<T>) => {
    // 遍历当前层级的所有节点
    for (const node of nodes as Array<T & {[propName: string]: any}>) {
      // 创建节点副本，移除 children 字段
      const {[childrenFieldName]: children, ...nodeWithoutChildren} = node
      // 将当前节点添加到结果数组
      result.push(nodeWithoutChildren as R)
      // 如果存在子节点，递归处理
      if (!isNone(children) && isArr(children) && !isEmptyArr(children)) flatten(children)
    }
  }
  // 对树形结构数组进行扁平化处理
  flatten(tree)
  // 返回结果数组
  return result
}


/**
 * 将数组转化为映射表
 *
 * @template T 数组元素的类型
 * @param {T[]} origin 原数组
 * @param {string | ((item: T, index: number) => (string | number | symbol))} primaryFieldName 数组元素的主键字段名，数组转化
 * 为映射表时，数组元素的主键字段值将会作为映射表的 key，映射表的 key 需要的类型为 `string | number | symbol`，如果传入的是字符串数据作为
 * 参数，则该参数为数组元素的主键字段名，如果传入的是函数类型作为参数，则该函数将会接收当前正在遍历处理的数组元素及其索引作为参数，该函数需要返
 * 回该元素在映射表中 key
 * @returns {Record<string | number | symbol, T>} 数组转化后的映射表
 */
export const convertArr2Map = <T>(
  origin: T[],
  primaryFieldName: string | ((item: T) => string | number | symbol)
): Record<(string | number | symbol), T> => {
  // 最终要返回的映射表结果
  const result: Record<(string | number | symbol), T> = {}
  // 获取主键字段值的方法
  const getPrimaryFieldVal =
    isFun(primaryFieldName) ?
      (item: T, index: number) => (primaryFieldName as Function)(item, index) :
      (item: T) => (item as T & {[propName: string]: any})[primaryFieldName]
  for (let i = 0; i < origin.length; i++) {
    // 将当前遍历的数组元素添加到映射表中
    result[getPrimaryFieldVal(origin[i], i)] = origin[i]
  }
  // 返回最终的映射表
  return result
}
